package at.chrenko.tu.vs.manager;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Scanner;

import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.util.encoders.Base64;

import at.chrenko.tu.vs.rmi.NotificationRMI;
import at.chrenko.tu.vs.shared.AESDecorator;
import at.chrenko.tu.vs.shared.Base64Decorator;
import at.chrenko.tu.vs.shared.Channel;
import at.chrenko.tu.vs.shared.Command;
import at.chrenko.tu.vs.shared.FileTask;
import at.chrenko.tu.vs.shared.InputHandler;
import at.chrenko.tu.vs.shared.Message;
import at.chrenko.tu.vs.shared.RSADecorator;
import at.chrenko.tu.vs.shared.SimpleChannel;

/**
 * @author Thomas Chrenko - 0728121
 *
 * SchedulerWorker processes the input from the scheduler
 * 
 */
public class SchedulerWorker extends Thread implements InputHandler {

	private InputStream in;
	private OutputStream out;
	private Channel channel;
	private FileTask task;
	private String message;
	private NotificationRMI callback;
	private Company company;
	private PrivateKey privateKey;
	private PublicKey publicKey;
	private final byte[] managerChallenge = new byte[32];
	private byte[] ivParam = new byte[16];
	final String B64 = "a-zA-Z0-9/+";
	private SecretKey key;
	
	public void setTask(FileTask task) {
		this.task = task;
	}
	
	public void setCallback(NotificationRMI callback) {
		this.callback = callback;
	}
	
	public void setCompany(Company company) {
		this.company = company;
	}
	
	public String getMessage() {
		return message;
	}
	
	public SchedulerWorker(InputStream in, OutputStream out, PrivateKey privateKey, PublicKey publicKey) throws NoSuchAlgorithmException, NoSuchPaddingException {//, FileTask task, NotificationRMI callback, Company company) {
		this.in = in;
		this.out = out;
		this.channel = new RSADecorator(new Base64Decorator(new SimpleChannel(in, out)), privateKey, publicKey);
		this.privateKey = privateKey;
		this.publicKey = publicKey;
//		this.task = task;
//		this.callback = callback;
//		this.company = company;
	}
	
	@Override
	public void run() {
		
		String firstMessage = Command.LOGIN + " ";
		
		SecureRandom secureRandom = new SecureRandom();
		
		secureRandom.nextBytes(managerChallenge);
		
		try {
			String managerChallengeBase64 = new String (Base64.encode(managerChallenge), "UTF-8");
			firstMessage = firstMessage + managerChallengeBase64;
			assert firstMessage.matches("!login ["+B64+"]{43}=") : "1st message";
			synchronized(this) {
				channel.send(firstMessage);
				channel = new RSADecorator(new Base64Decorator(new SimpleChannel(in, out)), privateKey, publicKey);
			}
		} catch (UnsupportedEncodingException e1) {} catch (IOException e) {} catch (NoSuchAlgorithmException e) {} catch (NoSuchPaddingException e) {}
		
		String inputLine;
		
		try {
			while ((inputLine = channel.receiveAsString()) != null) {//in.readLine()) != null) {   
				processInput(inputLine);
			}
		} catch (IOException e) {}
	}
	
	@Override
	public void processInput(String input) {
		if(input.startsWith(Command.OK)) {
			handleOK(input);
		}
		else if(input.startsWith(Command.REQUESTENGINE)) {
			handleRequestEngine(input);
		}
		else {
			System.out.println(Message.UNKNOWN_RESPONSE);
		}
	}
	
	public void handleOK(String input) {
		input = input.replaceFirst(Command.OK + " ", "");
		
		Scanner scanner = new Scanner(input);
		
		if(!scanner.hasNext()) {
			System.err.println("Error");
			return;
		}
		
		String managerChallengeBase64 = scanner.next();
		
		if(!scanner.hasNext()) {
			System.err.println("Error");
			return;
		}
		
		String schedulerChallengeBase64 = scanner.next();
		
		if(!scanner.hasNext()) {
			System.err.println("Error");
			return;
		}
		
		String secretKeyBase64 = scanner.next();
		
		if(!scanner.hasNext()) {
			System.err.println("Error");
			return;
		}
		
		String ivParamBase64 = scanner.next();
		
		if(scanner.hasNext()) {
			System.err.println("Error");
			return;
		}
		
		byte[] managerChallenge;
		
		try {
			managerChallenge = Base64.decode(managerChallengeBase64.getBytes("UTF-8"));
		} catch (UnsupportedEncodingException e) {
			System.err.println(e.getMessage());
			return;
		}
		
		if(!Arrays.equals(managerChallenge, this.managerChallenge)) {
			System.err.println("Error");
			return;
		}
		
		byte[] secretKey;
		
		try {
			secretKey = Base64.decode(secretKeyBase64.getBytes("UTF-8"));
			ivParam = Base64.decode(ivParamBase64.getBytes("UTF-8"));
		} catch (UnsupportedEncodingException e) {
			System.err.println(e.getMessage());
			return;
		}
		
		key = new SecretKeySpec(secretKey, "AES");
		
		try {
			channel = new AESDecorator(new Base64Decorator(new SimpleChannel(in, out)), key, ivParam);
		} catch (NoSuchAlgorithmException e) {
			System.err.println(e.getMessage());
			return;
		} catch (NoSuchPaddingException e) {
			System.err.println(e.getMessage());
			return;
		}
		
		String thirdMessage = schedulerChallengeBase64;
		
		assert thirdMessage.matches("["+B64+"]{43}=") : "3rd message";
		
		try {
			channel.send(thirdMessage);
		} catch (IOException e) {
			System.err.println(e.getMessage());
			return;
		}
	}
	
	/**
	 * Sends request command
	 * 
	 * If input is a valid request command it sends an engine request to the scheduler
	 * 
	 * @param input
	 */
	public void requestEngine(FileTask task) {
		StringBuilder request = new StringBuilder(Command.REQUESTENGINE);
		request.append(" ");
		request.append(task.getTaskType());
		request.append(" ");
		request.append(task.getId());
		request.append(" ");
		request.append(task.getAmount());
		
		try {
			channel.send(request.toString());
		} catch (IOException e) {}//out.println(request.toString());
	}
	
	/**
	 * Handles request command
	 * 
	 * @param input
	 * @return The assigned engine(s) if input is a positive request answer and the error messgage otherwise
	 */
	private void handleRequestEngine(String input) {
		input = input.replaceFirst(Command.REQUESTENGINE + " ", "");
		
		Scanner scanner = new Scanner(input);
		
		for(int i=0; i<task.getAmount(); i++) {
			if(!scanner.hasNext()) {
				message = input;
				notifyManager();
				return;
			}
			
			String address = scanner.next();
			
			if(!scanner.hasNextInt()){
				message = input;
				notifyManager();
				return;
			}
			
			int port = scanner.nextInt();
			
			try {
				if(!task.isDistributed())
					task.setAssignedEngine(i, InetAddress.getByName(address), port, task.getStartScript());
				else
					task.setAssignedEngine(i, InetAddress.getByName(address), port, task.getStartScript().replace("$a", String.valueOf(task.getAmount())).replace("$p", String.valueOf(i+1)));
			} catch (UnknownHostException e) {
				message = String.format("Error: Can not assign %s:%d.", address, port);
				notifyManager();
				return;
			}
		}
		
		if(!scanner.hasNextInt()){
			message = input;
			notifyManager();
			return;
		}
		
//		int taskID = 
		scanner.nextInt();
		
		if(scanner.hasNext()){
			message = input;
			notifyManager();
			return;
		}
		
//		try {
//			task.setAssignedAddress(InetAddress.getByName(address));
//		} catch (UnknownHostException e) {
//			message = String.format("Error: Can not assign %s:%d to task %d.", address, port, taskID);
//			notifyManager();
//			return;
//		}
		
//		task.setAssignedPort(port);
		
		ExecutionHandler exHandler = new ExecutionHandler(task, callback, company);
		exHandler.start();
		while(true)
			try {
				synchronized (exHandler) {
					exHandler.wait();
				}
				
				message = exHandler.getMessage();
				notifyManager();
				return;
			} catch (InterruptedException e) {}
	}
	
	private void notifyManager() {
		synchronized (this) {
			notify();
		}
	}
}
